Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
60.00% covered (warning)
60.00%
6 / 10
CRAP
80.00% covered (warning)
80.00%
68 / 85
ProductUpdater
0.00% covered (danger)
0.00%
0 / 1
60.00% covered (warning)
60.00%
6 / 10
54.45
80.00% covered (warning)
80.00%
68 / 85
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 update
0.00% covered (danger)
0.00%
0 / 1
3.47
62.50% covered (warning)
62.50%
5 / 8
 filterData
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 setData
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
21 / 21
 validateAssociationsDataType
0.00% covered (danger)
0.00%
0 / 1
8.12
50.00% covered (danger)
50.00%
8 / 16
 filterParentAssociations
0.00% covered (danger)
0.00%
0 / 1
3.07
80.00% covered (warning)
80.00%
4 / 5
 validateScalar
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
3 / 3
 validateScalarArray
0.00% covered (danger)
0.00%
0 / 1
9.29
44.44% covered (danger)
44.44%
4 / 9
 updateProductFields
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 addEmptyValues
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
10 / 10
<?php
declare(strict_types=1);
namespace Akeneo\Pim\Enrichment\Component\Product\Updater;
use Akeneo\Pim\Enrichment\Component\Product\Association\ParentAssociationsFilter;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Tool\Component\StorageUtils\Exception\InvalidObjectException;
use Akeneo\Tool\Component\StorageUtils\Exception\InvalidPropertyTypeException;
use Akeneo\Tool\Component\StorageUtils\Exception\UnknownPropertyException;
use Akeneo\Tool\Component\StorageUtils\Updater\ObjectUpdaterInterface;
use Akeneo\Tool\Component\StorageUtils\Updater\PropertySetterInterface;
use Doctrine\Common\Util\ClassUtils;
/**
 * Updates a product
 *
 * @author    Nicolas Dupont <nicolas@akeneo.com>
 * @copyright 2014 Akeneo SAS (http://www.akeneo.com)
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
class ProductUpdater implements ObjectUpdaterInterface
{
    /** @var PropertySetterInterface */
    protected $propertySetter;
    /** @var ObjectUpdaterInterface */
    protected $valuesUpdater;
    /** @var array */
    protected $ignoredFields = [];
    /** @var ParentAssociationsFilter */
    private $parentAssociationsFilter;
    /**
     * @param PropertySetterInterface  $propertySetter
     * @param ObjectUpdaterInterface   $valuesUpdater
     * @param ParentAssociationsFilter $parentAssociationsFilter
     * @param array                    $ignoredFields
     */
    public function __construct(
        PropertySetterInterface $propertySetter,
        ObjectUpdaterInterface $valuesUpdater,
        ParentAssociationsFilter $parentAssociationsFilter,
        array $ignoredFields
    ) {
        $this->propertySetter = $propertySetter;
        $this->valuesUpdater = $valuesUpdater;
        $this->ignoredFields = $ignoredFields;
        $this->parentAssociationsFilter = $parentAssociationsFilter;
    }
    /**
     * {@inheritdoc}
     *
     * {
     *      "identifier": "my-sku",
     *      "values": {
     *          "sku": [{
     *             "locale": null,
     *             "scope":  null,
     *             "data":  "my-sku",
     *          }],
     *          "name": [{
     *              "locale": "fr_FR",
     *              "scope":  null,
     *              "data":  "T-shirt super beau",
     *          }],
     *          "description": [
     *               {
     *                   "locale": "en_US",
     *                   "scope":  "mobile",
     *                   "data":   "My description"
     *               },
     *               {
     *                   "locale": "fr_FR",
     *                   "scope":  "mobile",
     *                   "data":   "Ma description mobile"
     *               },
     *               {
     *                   "locale": "en_US",
     *                   "scope":  "ecommerce",
     *                   "data":   "My description for the website"
     *               },
     *          ],
     *          "price": [
     *               {
     *                   "locale": null,
     *                   "scope":  ecommerce,
     *                   "data":   [
     *                       {"amount": 10, "currency": "EUR"},
     *                       {"amount": 24, "currency": "USD"},
     *                       {"amount: 20, "currency": "CHF"}
     *                   ]
     *               }
     *               {
     *                   "locale": null,
     *                   "scope":  mobile,
     *                   "data":   [
     *                       {"amount": 11, "currency": "EUR"},
     *                       {"amount": 25, "currency": "USD"},
     *                       {"amount": 21, "currency": "CHF"}
     *                   ]
     *               }
     *          ],
     *          "length": [{
     *              "locale": "en_US",
     *              "scope":  "mobile",
     *              "data":   {"amount": "10", "unit": "CENTIMETER"}
     *          }]
     *      },
     *      "enabled": true,
     *      "categories": ["tshirt", "men"],
     *      "associations": {
     *          "XSELL": {
     *              "groups": ["akeneo_tshirt", "oro_tshirt"],
     *              "product": ["AKN_TS", "ORO_TSH"]
     *          }
     *      },
     *      "parent_associations": {
     *          "XSELL": {
     *              "groups": ["foo_group", "bar_group"],
     *              "product": ["foo_product", "bar_product"]
     *          }
     *      }
     * }
     */
    public function update($product, array $data, array $options = []): ProductUpdater
    {
        if (!$product instanceof ProductInterface) {
            throw InvalidObjectException::objectExpected(
                ClassUtils::getClass($product),
                ProductInterface::class
            );
        }
        foreach ($data as $code => $value) {
            $filteredValue = $this->filterData($code, $value, $data);
            $this->setData($product, $code, $filteredValue, $options);
        }
        return $this;
    }
    /**
     * @param                  $field
     * @param                  $data
     * @param array            $context
     * @return array
     */
    protected function filterData($field, $data, array $context = [])
    {
        switch ($field) {
            case 'associations':
                $this->validateAssociationsDataType($data);
                if (isset($context['parent_associations'])) {
                    $data = $this->filterParentAssociations($data, $context['parent_associations']);
                }
                break;
        }
        return $data;
    }
    /**
     * @param ProductInterface $product
     * @param                  $field
     * @param                  $data
     * @param array            $options
     */
    protected function setData(ProductInterface $product, $field, $data, array $options = []): void
    {
        switch ($field) {
            case 'enabled':
            case 'family':
            case 'parent':
                $this->validateScalar($field, $data);
                $this->updateProductFields($product, $field, $data);
                break;
            case 'categories':
            case 'groups':
                $this->validateScalarArray($field, $data);
                $this->updateProductFields($product, $field, $data);
                break;
            case 'associations':
                $this->updateProductFields($product, $field, $data);
                break;
            case 'values':
                $this->valuesUpdater->update($product, $data, $options);
                $this->addEmptyValues($product, $data);
                break;
            default:
                if (!in_array($field, $this->ignoredFields)) {
                    throw UnknownPropertyException::unknownProperty($field);
                }
        }
    }
    /**
     * Validate association data
     *
     * @param $data
     *
     * @throws InvalidPropertyTypeException
     */
    protected function validateAssociationsDataType($data): void
    {
        if (!is_array($data)) {
            throw InvalidPropertyTypeException::arrayExpected(
                'associations',
                static::class,
                $data
            );
        }
        foreach ($data as $associationTypeCode => $associationTypeValues) {
            $this->validateScalar('associations', $associationTypeCode);
            if (!is_array($associationTypeValues)) {
                throw InvalidPropertyTypeException::arrayExpected(
                    'associations',
                    static::class,
                    $associationTypeValues
                );
            }
            foreach ($associationTypeValues as $property => $value) {
                $this->validateScalar('associations', $property);
                $this->validateScalarArray('associations', $value);
            }
        }
    }
    /**
     * Validate association data
     *
     * @param array $associations
     * @param array $parentAssociations
     *
     * @return array
     */
    protected function filterParentAssociations(array $associations, ?array $parentAssociations): array
    {
        if (null === $parentAssociations) {
            return $associations;
        }
        $associations = $this->parentAssociationsFilter->filterParentAssociations(
            $associations,
            $parentAssociations
        );
        return $associations;
    }
    /**
     * Validate that it is a scalar value.
     *
     * @param $field
     * @param $data
     *
     * @throws InvalidPropertyTypeException
     */
    protected function validateScalar($field, $data): void
    {
        if (null !== $data && !is_scalar($data)) {
            throw InvalidPropertyTypeException::scalarExpected($field, static::class, $data);
        }
    }
    /**
     * Validate that it is an array with scalar values.
     *
     * @param string $field
     * @param mixed  $data
     *
     * @throws InvalidPropertyTypeException
     */
    protected function validateScalarArray($field, $data): void
    {
        if (!is_array($data)) {
            throw InvalidPropertyTypeException::arrayExpected($field, static::class, $data);
        }
        foreach ($data as $value) {
            if (null !== $value && !is_scalar($value)) {
                throw InvalidPropertyTypeException::validArrayStructureExpected(
                    $field,
                    sprintf('one of the %s is not a scalar', $field),
                    static::class,
                    $data
                );
            }
        }
    }
    /**
     * Sets the field
     *
     * @param ProductInterface $product
     * @param string           $field
     * @param mixed            $value
     */
    protected function updateProductFields(ProductInterface $product, $field, $value): void
    {
        $this->propertySetter->setData($product, $field, $value);
    }
    /**
     * Add empty values coming from the family of the $product
     *
     * TODO: TEMPORARY FIX, AS API-108 WILL HANDLE EMPTY VALUES
     *
     * @param ProductInterface $product
     * @param array            $values
     */
    private function addEmptyValues(ProductInterface $product, array $values): void
    {
        $family = $product->getFamily();
        $authorizedCodes = (null !== $family) ? $family->getAttributeCodes() : [];
        foreach ($values as $code => $value) {
            $isFamilyAttribute = in_array($code, $authorizedCodes);
            foreach ($value as $data) {
                $emptyData = ('' === $data['data'] || [] === $data['data'] || null === $data['data']);
                if ($isFamilyAttribute && $emptyData) {
                    $options = ['locale' => $data['locale'], 'scope' => $data['scope']];
                    $this->propertySetter->setData($product, $code, $data['data'], $options);
                }
            }
        }
    }
}